Skip to main content

Go Map 为什么是非线程安全的?

Go map 默认是并发不安全的,同时对 map 进行并发读写的时,程序会 panic,原因如下:Go 官方经过长时间的讨论,认为 map 适配的场景应该是简单的(不需要从多个 gorountine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持。

并发读写可能引发的问题

func runWithPanic() {
s := make(map[int]int)
n := 100
for i := 0; i < n; i++ {
go func(i int) {
s[i] = i
}(i)
}
for i := 0; i <= n; i++ {
go func(i int) {
fmt.Printf("第 %d 个元素是 %v", i, s[i])
}(i)
}
time.Sleep(time.Second)
// fatal error: concurrent map writes
}

使用 sync.RWMutex 解决并发读写的问题

func runWithSyncRWMutex() {
var lock sync.RWMutex
s := make(map[int]int)
n := 100
for i := 0; i < n; i++ {
go func(i int) {
lock.Lock()
s[i] = i
lock.Unlock()
}(i)
}
for i := 0; i <= n; i++ {
go func(i int) {
lock.RLock()
fmt.Printf("第 %d 个元素是%v;", i, s[i])
lock.RUnlock()
}(i)
}
time.Sleep(time.Second)
}

使用 sync.Map 解决并发读写的问题

func RunWithSyncMap() {
s := sync.Map{}
n := 100
for i := 0; i < n; i++ {
go func(i int) {
s.Store(i, i)
}(i)
}
for i := 0; i <= n; i++ {
go func(i int) {
v, ok := s.Load(i)
if ok {
fmt.Printf("第 %d 个元素是%v;", i, v)
}
}(i)
}
time.Sleep(time.Second)
}

实际上,sync.Map 也是通过加锁的方式实现并发安全的,sync.Map 源码的数据结构如下:

type Map struct {
mu Mutex

// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Value // readOnly

// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[any]*entry

// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}

最后 就像官方考虑的那样,我们在使用中,应尽量避免对 Map 进行并发读写,尝试通过其他方式解决问题,如数据解耦、分布执行、动态规划等,真的需要并发读写时,为避免产生并发读写的问题,请使用锁的机制进行控制